<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>多音频合成工具</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/ffmpeg/0.11.6/ffmpeg.min.js"></script>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', Arial,
          sans-serif;
      }

      :root {
        --primary-bg: #e6e7ee;
        --secondary-bg: #ffffff;
        --shadow-light: #ffffff;
        --shadow-dark: #d1d9e6;
        --accent-color: #667eea;
        --accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        --success-color: #4ecdc4;
        --danger-color: #ff6b6b;
        --warning-color: #feca57;
        --text-primary: #2d3748;
        --text-secondary: #718096;
        --glass-bg: rgba(255, 255, 255, 0.25);
        --glass-border: rgba(255, 255, 255, 0.18);
      }

      body {
        background: var(--primary-bg);
        color: var(--text-primary);
        min-height: 100vh;
        padding: 20px;
        display: flex;
        flex-direction: column;
        align-items: center;
        transition: all 0.3s ease;
        animation: pageLoad 0.8s ease;
      }

      @keyframes pageLoad {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .container {
        width: 100%;
        max-width: 1200px;
        background: var(--primary-bg);
        border-radius: 30px;
        padding: 40px;
        box-shadow: 20px 20px 60px var(--shadow-dark),
          -20px -20px 60px var(--shadow-light);
        margin-top: 20px;
        transition: all 0.3s ease;
        animation: containerFloat 1s ease 0.2s both;
      }

      @keyframes containerFloat {
        from {
          opacity: 0;
          transform: translateY(30px) scale(0.95);
        }
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }

      .container:hover {
        box-shadow: 25px 25px 70px var(--shadow-dark),
          -25px -25px 70px var(--shadow-light);
      }

      header {
        text-align: center;
        margin-bottom: 40px;
        padding: 30px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: headerSlide 0.8s ease 0.4s both;
      }

      @keyframes headerSlide {
        from {
          opacity: 0;
          transform: translateY(-30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      h1 {
        font-size: 3.2rem;
        margin-bottom: 15px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        font-weight: 700;
        animation: titleGlow 3s ease-in-out infinite alternate;
      }

      @keyframes titleGlow {
        from {
          filter: brightness(1);
        }
        to {
          filter: brightness(1.2);
        }
      }

      .subtitle {
        font-size: 1.3rem;
        color: var(--text-secondary);
        margin-bottom: 30px;
        font-weight: 500;
      }

      .panel {
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        transition: all 0.3s ease;
        position: relative;
        overflow: hidden;
        margin-bottom: 30px;
      }

      .panel::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.1),
          transparent
        );
        transition: left 0.8s;
      }

      .panel:hover::before {
        left: 100%;
      }

      .panel:hover {
        transform: translateY(-5px);
        box-shadow: 16px 16px 32px var(--shadow-dark),
          -16px -16px 32px var(--shadow-light);
      }

      .panel-title {
        font-size: 1.6rem;
        margin-bottom: 25px;
        color: var(--accent-color);
        display: flex;
        align-items: center;
        font-weight: 600;
      }

      .panel-title::before {
        content: attr(data-icon);
        margin-right: 12px;
        font-size: 1.8rem;
        animation: iconBounce 2s ease-in-out infinite;
      }

      @keyframes iconBounce {
        0%,
        100% {
          transform: scale(1);
        }
        50% {
          transform: scale(1.1);
        }
      }

      .file-upload {
        border: 3px dashed var(--accent-color);
        border-radius: 25px;
        padding: 50px 40px;
        text-align: center;
        transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        margin-bottom: 25px;
        cursor: pointer;
        background: var(--primary-bg);
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }

      .file-upload:hover {
        border-color: var(--success-color);
        transform: translateY(-5px) scale(1.02);
        box-shadow: inset 10px 10px 20px var(--shadow-dark),
          inset -10px -10px 20px var(--shadow-light),
          0 15px 40px rgba(102, 126, 234, 0.25), 0 5px 15px rgba(0, 0, 0, 0.1);
      }

      .file-upload.dragover {
        border-color: var(--success-color);
        background: rgba(78, 205, 196, 0.05);
        transform: scale(1.05);
        box-shadow: inset 12px 12px 24px var(--shadow-dark),
          inset -12px -12px 24px var(--shadow-light),
          0 20px 50px rgba(78, 205, 196, 0.3);
        animation: dragPulse 1s ease-in-out infinite;
      }

      @keyframes dragPulse {
        0%,
        100% {
          border-color: var(--success-color);
        }
        50% {
          border-color: var(--accent-color);
        }
      }

      .file-upload-content {
        position: relative;
        z-index: 1;
      }

      .file-upload-icon {
        font-size: 3.5rem;
        margin-bottom: 20px;
        display: block;
        animation: iconFloat 3s ease-in-out infinite;
        filter: drop-shadow(0 4px 8px rgba(102, 126, 234, 0.2));
      }

      @keyframes iconFloat {
        0%,
        100% {
          transform: translateY(0);
        }
        50% {
          transform: translateY(-10px);
        }
      }

      .file-upload-text {
        font-size: 1.4rem;
        font-weight: 600;
        color: var(--text-primary);
        margin-bottom: 10px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
      }

      .file-upload-subtext {
        font-size: 1.1rem;
        color: var(--text-secondary);
        margin-bottom: 20px;
        font-weight: 500;
      }

      .file-upload-formats {
        font-size: 0.9rem;
        color: var(--accent-color);
        font-weight: 500;
        padding: 8px 16px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 20px;
        display: inline-block;
        margin-bottom: 20px;
      }

      .file-upload input[type='file'] {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        opacity: 0;
        cursor: pointer;
        z-index: 2;
      }

      .audio-list {
        margin-top: 20px;
      }

      .audio-item {
        display: flex;
        align-items: center;
        padding: 15px;
        background: var(--primary-bg);
        border-radius: 15px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        margin-bottom: 15px;
        transition: all 0.3s ease;
      }

      .audio-item:hover {
        transform: translateY(-2px);
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }

      .audio-info {
        flex: 1;
        margin-right: 15px;
      }

      .audio-name {
        font-weight: 600;
        color: var(--text-primary);
        margin-bottom: 5px;
      }

      .audio-details {
        font-size: 0.9rem;
        color: var(--text-secondary);
        display: flex;
        gap: 15px;
      }

      .audio-controls {
        display: flex;
        align-items: center;
        gap: 10px;
      }

      .audio-preview {
        width: 200px;
      }

      .audio-preview audio {
        width: 100%;
        height: 40px;
      }

      .remove-btn {
        background: var(--danger-color);
        color: white;
        border: none;
        padding: 8px 12px;
        border-radius: 10px;
        cursor: pointer;
        font-size: 0.9rem;
        transition: all 0.3s ease;
      }

      .remove-btn:hover {
        background: #e55555;
        transform: scale(1.05);
      }

      .options-form {
        display: flex;
        gap: 20px;
        margin-bottom: 25px;
        flex-wrap: wrap;
        align-items: flex-end;
      }

      .form-group {
        flex: 1;
        min-width: 150px;
      }

      label {
        display: block;
        margin-bottom: 10px;
        color: var(--text-secondary);
        font-weight: 500;
        font-size: 0.95rem;
      }

      input,
      select {
        width: 100%;
        padding: 14px 18px;
        background: var(--primary-bg);
        border: none;
        border-radius: 15px;
        color: var(--text-primary);
        font-size: 1rem;
        font-weight: 500;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        transition: all 0.3s ease;
      }

      input:focus,
      select:focus {
        outline: none;
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light),
          0 0 0 3px rgba(102, 126, 234, 0.2);
      }

      button {
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 14px 28px;
        border-radius: 15px;
        cursor: pointer;
        font-weight: 600;
        transition: all 0.3s ease;
        font-size: 1rem;
        flex-shrink: 0;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }

      button::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.2),
          transparent
        );
        transition: left 0.5s;
      }

      button:hover::before {
        left: 100%;
      }

      button:hover {
        transform: translateY(-3px);
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
      }

      button:active {
        transform: translateY(0);
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }

      button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
        transform: none;
        box-shadow: 4px 4px 8px var(--shadow-dark),
          -4px -4px 8px var(--shadow-light);
      }

      .btn-primary {
        background: var(--accent-gradient);
        color: white;
        padding: 16px 32px;
        font-size: 1.1rem;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
      }

      .btn-primary:hover {
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2),
          0 0 30px rgba(102, 126, 234, 0.3);
      }

      .btn-success {
        background: linear-gradient(
          135deg,
          var(--success-color) 0%,
          #44a08d 100%
        );
        color: white;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
      }

      .btn-secondary {
        background: var(--warning-color);
        color: white;
        padding: 12px 24px;
        font-size: 0.95rem;
      }

      .progress-container {
        margin: 30px 0;
        animation: progressSlide 0.8s ease 0.8s both;
      }

      @keyframes progressSlide {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .progress-bar {
        height: 16px;
        background: var(--primary-bg);
        border-radius: 20px;
        overflow: hidden;
        margin-bottom: 15px;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        position: relative;
      }

      .progress-fill {
        height: 100%;
        background: var(--accent-gradient);
        width: 0%;
        transition: width 0.3s ease;
        border-radius: 20px;
        box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
        position: relative;
        overflow: hidden;
      }

      .progress-fill::after {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.3),
          transparent
        );
        animation: progressShine 2s infinite;
      }

      @keyframes progressShine {
        0% {
          left: -100%;
        }
        100% {
          left: 100%;
        }
      }

      .progress-text {
        text-align: center;
        font-size: 1rem;
        color: var(--text-secondary);
        font-weight: 500;
      }

      .actions {
        display: flex;
        justify-content: center;
        gap: 25px;
        margin-top: 30px;
        animation: actionsSlide 0.8s ease 1s both;
      }

      @keyframes actionsSlide {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .result-panel {
        text-align: center;
        animation: resultPanelSlide 0.8s ease 1.2s both;
      }

      @keyframes resultPanelSlide {
        from {
          opacity: 0;
          transform: translateY(30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .status-message {
        min-height: 35px;
        margin: 20px 0;
        color: var(--accent-color);
        font-weight: 600;
        font-size: 1.1rem;
        transition: all 0.3s ease;
      }

      .status-message.processing {
        animation: statusPulse 1.5s ease-in-out infinite;
      }

      @keyframes statusPulse {
        0%,
        100% {
          opacity: 1;
        }
        50% {
          opacity: 0.6;
        }
      }

      .error-message {
        color: var(--danger-color);
        margin: 20px 0;
        min-height: 35px;
        font-weight: 600;
        font-size: 1.1rem;
        animation: errorShake 0.5s ease;
      }

      @keyframes errorShake {
        0%,
        100% {
          transform: translateX(0);
        }
        25% {
          transform: translateX(-5px);
        }
        75% {
          transform: translateX(5px);
        }
      }

      .timeline-section {
        margin-top: 20px;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        border: 2px solid var(--warning-color);
      }

      .timeline-title {
        font-size: 1.2rem;
        font-weight: 600;
        color: var(--text-primary);
        margin-bottom: 20px;
        display: flex;
        align-items: center;
        gap: 8px;
      }

      .timeline-content {
        background: var(--primary-bg);
        border-radius: 15px;
        padding: 20px;
        margin-bottom: 20px;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        max-height: 300px;
        overflow-y: auto;
      }

      .timeline-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 12px 0;
        border-bottom: 1px solid rgba(102, 126, 234, 0.1);
        font-family: 'Courier New', monospace;
      }

      .timeline-item:last-child {
        border-bottom: none;
      }

      .timeline-filename {
        font-weight: 600;
        color: var(--text-primary);
        flex: 1;
        margin-right: 15px;
      }

      .timeline-time {
        color: var(--accent-color);
        font-weight: 500;
        font-size: 0.9rem;
      }

      .timeline-actions {
        text-align: center;
      }

      .download-section {
        display: flex;
        flex-direction: column;
        gap: 20px;
        margin-top: 20px;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        border: 2px solid var(--accent-color);
      }

      .download-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 15px;
        background: var(--primary-bg);
        border-radius: 15px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        transition: all 0.3s ease;
        border: 1px solid rgba(102, 126, 234, 0.1);
      }

      .download-item:hover {
        transform: translateY(-1px);
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        border-color: var(--accent-color);
      }

      .download-title {
        font-size: 1rem;
        font-weight: 600;
        color: var(--text-primary);
        display: flex;
        align-items: center;
        gap: 8px;
      }

      .download-size {
        font-size: 0.9rem;
        color: var(--text-secondary);
        font-weight: 500;
        padding: 6px 12px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 15px;
        border: 1px solid rgba(102, 126, 234, 0.2);
        margin: 0 10px;
      }

      .instructions {
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        margin-top: 40px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: instructionsSlide 0.8s ease 1.4s both;
      }

      @keyframes instructionsSlide {
        from {
          opacity: 0;
          transform: translateY(30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .instructions h3 {
        color: var(--accent-color);
        margin-bottom: 20px;
        font-size: 1.4rem;
        font-weight: 600;
      }

      .instructions ol {
        padding-left: 25px;
        margin-bottom: 20px;
      }

      .instructions li {
        margin-bottom: 12px;
        line-height: 1.7;
        color: var(--text-secondary);
        font-weight: 500;
      }

      .instructions p {
        color: var(--text-secondary);
        font-style: italic;
        font-weight: 500;
        padding: 15px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 15px;
        border-left: 4px solid var(--accent-color);
      }

      footer {
        margin-top: 40px;
        text-align: center;
        color: var(--text-secondary);
        font-size: 0.95rem;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 20px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        animation: footerFade 0.8s ease 1.6s both;
      }

      @keyframes footerFade {
        from {
          opacity: 0;
        }
        to {
          opacity: 1;
        }
      }

      @media (max-width: 768px) {
        .container {
          padding: 20px;
          margin: 10px;
        }

        h1 {
          font-size: 2.5rem;
        }

        .subtitle {
          font-size: 1.1rem;
        }

        .options-form {
          flex-direction: column;
        }

        .form-group {
          min-width: 100%;
        }

        .actions {
          flex-direction: column;
          align-items: center;
        }

        .audio-item {
          flex-direction: column;
          align-items: flex-start;
          gap: 15px;
        }

        .audio-controls {
          width: 100%;
          justify-content: space-between;
        }

        .audio-preview {
          width: 100%;
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <header>
        <h1>🎵 多音频合成工具</h1>
        <p class="subtitle">
          将多个音频文件合成为一个音频文件，支持串联和混合模式
        </p>
      </header>

      <div class="panel">
        <h2 class="panel-title" data-icon="📁">音频文件上传</h2>
        <div class="file-upload" id="audioUpload">
          <div class="file-upload-content">
            <span class="file-upload-icon">🎵</span>
            <div class="file-upload-text">点击或拖拽音频文件到此处</div>
            <div class="file-upload-subtext">
              支持多文件选择和拖拽上传，单个文件最大 200MB
            </div>
            <div class="file-upload-formats">
              支持格式：MP3, WAV, AAC, OGG, M4A, FLAC
            </div>
          </div>
          <input
            type="file"
            id="audioInput"
            accept=".mp3,.wav,.aac,.ogg,.m4a,.flac"
            multiple
          />
        </div>

        <div class="audio-list" id="audioList"></div>

        <div class="actions" style="margin-top: 20px">
          <button class="btn-secondary" id="clearAllBtn" style="display: none">
            🗑️ 清空所有
          </button>
        </div>
      </div>

      <div class="panel" id="settingsPanel" style="display: none">
        <h2 class="panel-title" data-icon="⚙️">合成设置</h2>
        <div class="options-form">
          <div class="form-group">
            <label for="mergeMode">合成模式</label>
            <select id="mergeMode">
              <option value="concat">串联模式 (依次播放)</option>
              <option value="mix">混合模式 (同时播放)</option>
            </select>
          </div>
          <div class="form-group">
            <label for="outputFormat">输出格式</label>
            <select id="outputFormat">
              <option value="mp3">MP3</option>
              <option value="wav">WAV</option>
              <option value="aac">AAC</option>
              <option value="ogg">OGG</option>
            </select>
          </div>
          <div class="form-group">
            <label for="bitrate">音频质量</label>
            <select id="bitrate">
              <option value="128k">128 kbps (标准)</option>
              <option value="192k">192 kbps (高质量)</option>
              <option value="320k" selected>320 kbps (最高质量)</option>
            </select>
          </div>
          <div class="form-group" id="crossfadeGroup" style="display: none">
            <label for="crossfade">交叉淡化时间 (秒)</label>
            <input
              type="number"
              id="crossfade"
              min="0"
              max="10"
              step="0.5"
              value="1"
              placeholder="淡化时间"
            />
          </div>
        </div>
      </div>

      <div class="actions">
        <button class="btn-primary" id="processBtn" disabled>
          🎵 开始合成音频
        </button>
      </div>

      <div
        class="progress-container"
        id="progressContainer"
        style="display: none"
      >
        <div class="progress-bar">
          <div class="progress-fill" id="progressFill"></div>
        </div>
        <div class="progress-text" id="progressText">准备中...</div>
      </div>

      <div class="status-message" id="statusMessage"></div>
      <div class="error-message" id="errorMessage"></div>

      <div class="result-panel" id="resultPanel" style="display: none">
        <!-- 时间轴信息面板 -->
        <div
          class="timeline-section"
          id="timelineSection"
          style="display: none"
        >
          <div class="timeline-title">⏱️ 音频时间轴信息</div>
          <div class="timeline-content" id="timelineContent">
            <!-- 时间轴信息将在这里显示 -->
          </div>
          <div class="timeline-actions">
            <button class="btn-secondary" id="downloadTimelineBtn">
              下载时间轴 (TXT)
            </button>
          </div>
        </div>

        <div class="download-section">
          <div class="download-item">
            <div class="download-title">🎵 合成音频文件</div>
            <div class="download-size" id="downloadSize">-</div>
            <button class="btn-success" id="downloadBtn">下载文件</button>
          </div>
          <div class="audio-preview">
            <audio
              controls
              id="resultAudio"
              style="width: 100%; margin-top: 15px"
            ></audio>
          </div>
        </div>
      </div>

      <div class="instructions">
        <h3>使用说明</h3>
        <ol>
          <li>选择或拖拽多个音频文件到上传区域</li>
          <li>预览已上传的音频文件，可以调整顺序或删除不需要的文件</li>
          <li>选择合成模式：串联模式将音频依次连接，混合模式将音频同时播放</li>
          <li>设置输出格式和音频质量</li>
          <li>点击"开始合成音频"按钮</li>
          <li>等待处理完成后预览和下载结果文件</li>
        </ol>
        <p>
          注意：串联模式适合制作音频集合，混合模式适合制作背景音乐混音。所有处理都在本地完成，保护您的隐私。
        </p>
      </div>

      <footer>
        <p>🔒 本地处理，保护隐私 | 🚀 基于 FFmpeg.wasm 技术</p>
      </footer>
    </div>

    <script>
      let ffmpeg = null
      let audioFiles = []
      let isProcessing = false

      // 初始化 FFmpeg
      async function initFFmpeg() {
        if (!ffmpeg) {
          ffmpeg = FFmpeg.createFFmpeg({
            log: true,
          })
        }

        if (!ffmpeg.isLoaded()) {
          await ffmpeg.load()
        }
      }

      // 文件上传处理
      function setupFileUpload() {
        const uploadArea = document.getElementById('audioUpload')
        const fileInput = document.getElementById('audioInput')
        const processBtn = document.getElementById('processBtn')
        const settingsPanel = document.getElementById('settingsPanel')
        const clearAllBtn = document.getElementById('clearAllBtn')

        // 点击上传
        uploadArea.addEventListener('click', (e) => {
          if (e.target !== fileInput) {
            e.preventDefault()
            fileInput.click()
          }
        })

        // 文件选择
        fileInput.addEventListener('change', handleFileSelect)

        // 拖拽上传
        uploadArea.addEventListener('dragover', (e) => {
          e.preventDefault()
          uploadArea.classList.add('dragover')
        })

        uploadArea.addEventListener('dragleave', (e) => {
          e.preventDefault()
          uploadArea.classList.remove('dragover')
        })

        uploadArea.addEventListener('drop', (e) => {
          e.preventDefault()
          uploadArea.classList.remove('dragover')
          const files = Array.from(e.dataTransfer.files)
          if (files.length > 0) {
            handleFiles(files)
          }
        })

        // 清空所有文件
        clearAllBtn.addEventListener('click', () => {
          audioFiles = []
          updateAudioList()
          updateUI()
        })

        function handleFileSelect(e) {
          const files = Array.from(e.target.files)
          if (files.length > 0) {
            handleFiles(files)
          }
          // 清空input的值，确保相同文件可以重复选择
          fileInput.value = ''
        }

        function handleFiles(files) {
          const validFiles = []

          for (const file of files) {
            // 检查文件大小（200MB限制）
            if (file.size > 200 * 1024 * 1024) {
              showError(`文件 ${file.name} 大小超过200MB限制`)
              continue
            }

            // 检查文件类型
            const allowedTypes = [
              'audio/mpeg',
              'audio/wav',
              'audio/aac',
              'audio/ogg',
              'audio/mp4',
              'audio/flac',
            ]
            if (
              !allowedTypes.includes(file.type) &&
              !file.name.match(/\.(mp3|wav|aac|ogg|m4a|flac)$/i)
            ) {
              showError(`不支持的音频格式: ${file.name}`)
              continue
            }

            // 检查是否已存在
            if (
              audioFiles.some(
                (af) => af.name === file.name && af.size === file.size
              )
            ) {
              showError(`文件 ${file.name} 已存在`)
              continue
            }

            validFiles.push(file)
          }

          if (validFiles.length > 0) {
            audioFiles.push(...validFiles)
            updateAudioList()
            updateUI()
            showStatus(`成功添加 ${validFiles.length} 个音频文件`)
            clearError()
          }
        }
      }

      // 更新音频列表显示
      function updateAudioList() {
        const audioList = document.getElementById('audioList')
        audioList.innerHTML = ''

        audioFiles.forEach((file, index) => {
          const audioItem = document.createElement('div')
          audioItem.className = 'audio-item'
          audioItem.innerHTML = `
            <div class="audio-info">
              <div class="audio-name">${file.name}</div>
              <div class="audio-details">
                <span>大小: ${formatFileSize(file.size)}</span>
                <span>格式: ${getFileExtension(file.name).toUpperCase()}</span>
                <span>序号: ${index + 1}</span>
              </div>
            </div>
            <div class="audio-controls">
              <div class="audio-preview">
                <audio controls preload="metadata">
                  <source src="${URL.createObjectURL(file)}" type="${
            file.type
          }">
                </audio>
              </div>
              <button class="remove-btn" onclick="removeAudioFile(${index})">
                🗑️ 删除
              </button>
            </div>
          `
          audioList.appendChild(audioItem)
        })
      }

      // 删除音频文件
      function removeAudioFile(index) {
        audioFiles.splice(index, 1)
        updateAudioList()
        updateUI()
        showStatus(`已删除音频文件`)
      }

      // 更新UI状态
      function updateUI() {
        const processBtn = document.getElementById('processBtn')
        const settingsPanel = document.getElementById('settingsPanel')
        const clearAllBtn = document.getElementById('clearAllBtn')

        if (audioFiles.length >= 2) {
          processBtn.disabled = false
          settingsPanel.style.display = 'block'
          clearAllBtn.style.display = 'inline-block'
        } else {
          processBtn.disabled = true
          settingsPanel.style.display = 'none'
          clearAllBtn.style.display = 'none'
        }
      }

      // 处理音频合成
      async function processAudioMerge() {
        if (audioFiles.length < 2 || isProcessing) return

        isProcessing = true
        const processBtn = document.getElementById('processBtn')
        const progressContainer = document.getElementById('progressContainer')
        const resultPanel = document.getElementById('resultPanel')

        processBtn.disabled = true
        progressContainer.style.display = 'block'
        resultPanel.style.display = 'none'

        try {
          showStatus('正在初始化 FFmpeg...')
          updateProgress(0, '初始化中...')

          await initFFmpeg()

          showStatus('正在处理音频文件...')
          updateProgress(10, '读取音频文件...')

          // 读取所有音频文件
          const inputFiles = []
          for (let i = 0; i < audioFiles.length; i++) {
            const file = audioFiles[i]
            const audioData = new Uint8Array(await file.arrayBuffer())
            const inputFileName = `input_${i}.${getFileExtension(file.name)}`
            ffmpeg.FS('writeFile', inputFileName, audioData)
            inputFiles.push(inputFileName)
          }

          updateProgress(30, '准备合成参数...')

          const mergeMode = document.getElementById('mergeMode').value
          const outputFormat = document.getElementById('outputFormat').value
          const bitrate = document.getElementById('bitrate').value
          const crossfade =
            parseFloat(document.getElementById('crossfade').value) || 0

          const outputFileName = `output.${outputFormat}`

          updateProgress(
            40,
            `开始${mergeMode === 'concat' ? '串联' : '混合'}处理...`
          )

          let args = []

          if (mergeMode === 'concat') {
            // 串联模式
            if (crossfade > 0 && audioFiles.length > 1) {
              // 带交叉淡化的串联
              let filterComplex = ''
              let inputs = ''

              for (let i = 0; i < inputFiles.length; i++) {
                args.push('-i', inputFiles[i])
                inputs += `[${i}:a]`
              }

              // 构建交叉淡化滤镜
              let currentLabel = '[0:a]'
              for (let i = 1; i < inputFiles.length; i++) {
                const nextLabel = `[mixed${i}]`
                filterComplex += `${currentLabel}[${i}:a]acrossfade=d=${crossfade}${nextLabel};`
                currentLabel = nextLabel
              }

              filterComplex = filterComplex.slice(0, -1) // 移除最后的分号

              args.push('-filter_complex', filterComplex)
              args.push('-map', `[mixed${inputFiles.length - 1}]`)
            } else {
              // 简单串联
              const concatFile = 'concat_list.txt'
              let concatContent = ''

              for (const inputFile of inputFiles) {
                args.push('-i', inputFile)
                concatContent += `file '${inputFile}'\n`
              }

              ffmpeg.FS('writeFile', concatFile, concatContent)
              args = ['-f', 'concat', '-safe', '0', '-i', concatFile]
            }
          } else {
            // 混合模式
            for (const inputFile of inputFiles) {
              args.push('-i', inputFile)
            }

            // 构建混合滤镜
            let filterComplex = ''
            for (let i = 0; i < inputFiles.length; i++) {
              filterComplex += `[${i}:a]`
            }
            filterComplex += `amix=inputs=${inputFiles.length}:duration=longest[mixed]`

            args.push('-filter_complex', filterComplex)
            args.push('-map', '[mixed]')
          }

          // 添加输出参数
          args.push('-c:a', getAudioCodec(outputFormat))
          args.push('-b:a', bitrate)
          args.push('-y', outputFileName)

          // 设置进度监听
          ffmpeg.setLogger(({ message }) => {
            const timeMatch = message.match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/)
            if (timeMatch) {
              const timeStr = timeMatch[1]
              updateProgress(50, `处理中... time=${timeStr} (参考时间)`)
            }
          })

          ffmpeg.setProgress(({ ratio }) => {
            const progress = Math.min(Math.max(40 + ratio * 50, 40), 90)
            // 只更新进度条，不更新文字
            progressFill.style.width = `${progress}%`
          })

          await ffmpeg.run(...args)

          updateProgress(95, '准备下载文件...')

          // 读取输出文件
          const outputData = ffmpeg.FS('readFile', outputFileName)

          // 创建下载链接和预览
          const blob = new Blob([outputData.buffer], {
            type: `audio/${outputFormat}`,
          })
          const url = URL.createObjectURL(blob)

          // 设置下载
          const downloadBtn = document.getElementById('downloadBtn')
          const downloadSize = document.getElementById('downloadSize')
          const resultAudio = document.getElementById('resultAudio')

          downloadBtn.onclick = () => {
            const a = document.createElement('a')
            a.href = url
            a.download = `merged_audio_${audioFiles.length}files.${outputFormat}`
            a.click()
          }

          downloadSize.textContent = formatFileSize(blob.size)
          resultAudio.src = url

          // 清理临时文件
          try {
            for (const inputFile of inputFiles) {
              ffmpeg.FS('unlink', inputFile)
            }
            ffmpeg.FS('unlink', outputFileName)
            if (mergeMode === 'concat' && crossfade === 0) {
              ffmpeg.FS('unlink', 'concat_list.txt')
            }
          } catch (e) {
            console.warn('清理临时文件失败:', e)
          }

          // 如果是串联模式，生成并显示时间轴信息
          if (mergeMode === 'concat') {
            updateProgress(98, '生成时间轴信息...')
            try {
              const timelineData = await generateTimelineData()
              displayTimelineInfo(timelineData)
            } catch (error) {
              console.warn('生成时间轴信息失败:', error)
            }
          }

          updateProgress(100, '处理完成！')
          showStatus(`音频合成完成！合成了 ${audioFiles.length} 个音频文件`)
          resultPanel.style.display = 'block'
        } catch (error) {
          console.error('处理失败:', error)
          showError(`处理失败: ${error.message}`)
          updateProgress(0, '处理失败')
        } finally {
          isProcessing = false
          processBtn.disabled = false
          setTimeout(() => {
            progressContainer.style.display = 'none'
          }, 2000)
        }
      }

      // 获取音频时长
      function getAudioDuration(file) {
        return new Promise((resolve) => {
          const audio = new Audio()
          audio.addEventListener('loadedmetadata', () => {
            resolve(audio.duration)
          })
          audio.addEventListener('error', () => {
            resolve(0) // 如果无法获取时长，返回0
          })
          audio.src = URL.createObjectURL(file)
        })
      }

      // 格式化时间显示
      function formatTime(seconds) {
        const hours = Math.floor(seconds / 3600)
        const minutes = Math.floor((seconds % 3600) / 60)
        const secs = Math.floor(seconds % 60)
        const ms = Math.floor((seconds % 1) * 1000)

        if (hours > 0) {
          return `${hours.toString().padStart(2, '0')}:${minutes
            .toString()
            .padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms
            .toString()
            .padStart(3, '0')}`
        } else {
          return `${minutes.toString().padStart(2, '0')}:${secs
            .toString()
            .padStart(2, '0')}.${ms.toString().padStart(3, '0')}`
        }
      }

      // 生成时间轴数据
      async function generateTimelineData() {
        const timelineData = []
        let currentTime = 0
        const crossfade = parseFloat(document.getElementById('crossfade').value)

        for (let i = 0; i < audioFiles.length; i++) {
          const file = audioFiles[i]
          const duration = await getAudioDuration(file)

          const startTime = currentTime
          const endTime = currentTime + duration

          timelineData.push({
            filename: file.name,
            startTime: startTime,
            endTime: endTime,
            duration: duration,
          })

          // 计算下一个音频的开始时间（考虑交叉淡化）
          if (i < audioFiles.length - 1 && crossfade > 0) {
            currentTime = endTime - crossfade
          } else {
            currentTime = endTime
          }
        }

        return timelineData
      }

      // 显示时间轴信息
      function displayTimelineInfo(timelineData) {
        const timelineContent = document.getElementById('timelineContent')
        const timelineSection = document.getElementById('timelineSection')

        timelineContent.innerHTML = ''

        timelineData.forEach((item, index) => {
          const timelineItem = document.createElement('div')
          timelineItem.className = 'timeline-item'
          timelineItem.innerHTML = `
            <div class="timeline-filename">${index + 1}. ${item.filename}</div>
            <div class="timeline-time">${formatTime(
              item.startTime
            )} - ${formatTime(item.endTime)}</div>
          `
          timelineContent.appendChild(timelineItem)
        })

        timelineSection.style.display = 'block'

        // 设置TXT下载功能
        const downloadTimelineBtn = document.getElementById(
          'downloadTimelineBtn'
        )
        downloadTimelineBtn.onclick = () => {
          downloadTimelineAsTxt(timelineData)
        }
      }

      // 下载时间轴为TXT文件
      function downloadTimelineAsTxt(timelineData) {
        let txtContent = '音频合成时间轴信息\n'
        txtContent += '='.repeat(50) + '\n\n'
        txtContent += `生成时间: ${new Date().toLocaleString()}\n`
        txtContent += `音频文件数量: ${timelineData.length}\n`
        txtContent += `合成模式: 串联模式\n\n`

        const crossfade = parseFloat(document.getElementById('crossfade').value)
        if (crossfade > 0) {
          txtContent += `交叉淡化时长: ${crossfade}秒\n\n`
        }

        txtContent += '时间轴详情:\n'
        txtContent += '-'.repeat(50) + '\n'

        timelineData.forEach((item, index) => {
          txtContent += `${index + 1}. ${item.filename}\n`
          txtContent += `   开始时间: ${formatTime(item.startTime)}\n`
          txtContent += `   结束时间: ${formatTime(item.endTime)}\n`
          txtContent += `   音频时长: ${formatTime(item.duration)}\n\n`
        })

        const totalDuration =
          timelineData.length > 0
            ? timelineData[timelineData.length - 1].endTime
            : 0
        txtContent += `总时长: ${formatTime(totalDuration)}\n`

        // 创建并下载文件
        const blob = new Blob([txtContent], {
          type: 'text/plain;charset=utf-8',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = `audio_timeline_${new Date().getTime()}.txt`
        a.click()
        URL.revokeObjectURL(url)
      }

      // 获取音频编解码器
      function getAudioCodec(format) {
        const codecs = {
          mp3: 'libmp3lame',
          wav: 'pcm_s16le',
          aac: 'aac',
          ogg: 'libvorbis',
        }
        return codecs[format] || 'libmp3lame'
      }

      // 工具函数
      function getFileExtension(filename) {
        return filename.split('.').pop().toLowerCase()
      }

      function formatFileSize(bytes) {
        if (bytes === 0) return '0 Bytes'
        const k = 1024
        const sizes = ['Bytes', 'KB', 'MB', 'GB']
        const i = Math.floor(Math.log(bytes) / Math.log(k))
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
      }

      function updateProgress(percent, text) {
        const progressFill = document.getElementById('progressFill')
        const progressText = document.getElementById('progressText')
        progressFill.style.width = percent + '%'
        progressText.textContent = text
      }

      function showStatus(message) {
        const statusElement = document.getElementById('statusMessage')
        statusElement.textContent = message
        statusElement.className = 'status-message processing'
      }

      function showError(message) {
        const errorElement = document.getElementById('errorMessage')
        errorElement.textContent = message
        setTimeout(() => {
          errorElement.textContent = ''
        }, 5000)
      }

      function clearError() {
        document.getElementById('errorMessage').textContent = ''
      }

      // 合成模式变化处理
      function handleMergeModeChange() {
        const mergeMode = document.getElementById('mergeMode').value
        const crossfadeGroup = document.getElementById('crossfadeGroup')

        if (mergeMode === 'concat') {
          crossfadeGroup.style.display = 'block'
        } else {
          crossfadeGroup.style.display = 'none'
        }
      }

      // 初始化
      document.addEventListener('DOMContentLoaded', () => {
        setupFileUpload()

        // 处理按钮事件
        document
          .getElementById('processBtn')
          .addEventListener('click', processAudioMerge)

        // 时间轴下载按钮事件
        document
          .getElementById('downloadTimelineBtn')
          .addEventListener('click', downloadTimelineAsTxt)

        // 合成模式变化事件
        document
          .getElementById('mergeMode')
          .addEventListener('change', handleMergeModeChange)

        // 预加载 FFmpeg
        initFFmpeg().catch(console.error)
      })
    </script>
  </body>
</html>
